M2.855 · Modelos avanzados de minería de datos · PEC2
2023-1 · Máster universitario en Ciencia de datos (Data science)
Estudios de Informática, Multimedia y Telecomunicación
Importante: la entrega debe contener el notebook (.ipynb) y su HTML tras la completa ejecución secuencial (.html) donde se pueda ver el código y los resultados. Para exportar el notebook a HTML puedes hacerlo desde el menú File → Download as → HTML.
A lo largo de esta práctica veremos como aplicar distintas técnicas no supervisadas así como algunas de sus aplicaciones reales:
!pip install umap-learn
!pip install --upgrade hdbscan
Requirement already satisfied: umap-learn in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (0.5.4) Requirement already satisfied: numpy>=1.17 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from umap-learn) (1.24.3) Requirement already satisfied: scipy>=1.3.1 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from umap-learn) (1.11.3) Requirement already satisfied: scikit-learn>=0.22 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from umap-learn) (1.3.0) Requirement already satisfied: numba>=0.51.2 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from umap-learn) (0.58.0) Requirement already satisfied: pynndescent>=0.5 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from umap-learn) (0.5.10) Requirement already satisfied: tqdm in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from umap-learn) (4.65.0) Requirement already satisfied: llvmlite<0.42,>=0.41.0dev0 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from numba>=0.51.2->umap-learn) (0.41.0) Requirement already satisfied: joblib>=0.11 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from pynndescent>=0.5->umap-learn) (1.2.0) Requirement already satisfied: threadpoolctl>=2.0.0 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from scikit-learn>=0.22->umap-learn) (2.2.0) Requirement already satisfied: hdbscan in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (0.8.33) Requirement already satisfied: cython<3,>=0.27 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from hdbscan) (0.29.36) Requirement already satisfied: numpy>=1.20 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from hdbscan) (1.24.3) Requirement already satisfied: scipy>=1.0 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from hdbscan) (1.11.3) Requirement already satisfied: scikit-learn>=0.20 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from hdbscan) (1.3.0) Requirement already satisfied: joblib>=1.0 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from hdbscan) (1.2.0) Requirement already satisfied: threadpoolctl>=2.0.0 in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (from scikit-learn>=0.20->hdbscan) (2.2.0)
Para ello vamos a necesitar las siguientes librerías:
import random
import tqdm
import umap
import numpy as np
import pandas as pd
import sklearn
from sklearn import cluster # Algoritmos de clustering.
from sklearn import datasets # Crear datasets.
from sklearn import decomposition # Algoritmos de reduccion de dimensionalidad.
# Visualizacion.
import matplotlib
import matplotlib.pyplot as plt
import plotly.graph_objects as go
import plotly.express as px
%matplotlib inline
Este ejercicio trata de explorar distintas técnicas de agrupamiento ajustándolas a distintos conjuntos de datos.
El objetivo es doble: entender la influencia de los parámetros en su comportamiento, y conocer sus limitaciones en la búsqueda de estructuras de datos.
X_blobs, y_blobs = datasets.make_blobs(n_samples=1000, n_features=2, centers=4, cluster_std=1.6, random_state=42)
X_moons, y_moons = datasets.make_moons(n_samples=1000, noise=.07, random_state=42)
X_circles, y_circles = datasets.make_circles(n_samples=1000, factor=.5, noise=.05, random_state=42)
Cada dataset tiene 2 variables: una variable X que contiene 2 features (columnas) y tantas filas como muestras. Y una variable y que alberga las etiquetas que identifican cada cluster.
A lo largo del ejercicio no se usará la variable y (sólo con el objetivo de visualizar). El objetivo es a través de los distintos modelos de clustering conseguir encontrar las estructuras descritas por las variables y.
fig, axis = plt.subplots(2, 3, figsize=(13, 8))
for i, (X, y, ax, name) in enumerate(zip([X_blobs, X_moons, X_circles] * 2,
[None] * 3 + [y_blobs, y_moons, y_circles],
axis.reshape(-1),
['Blob', 'Moons', 'Circles'] * 2)):
ax.set_title('Dataset: {}, '.format(name) + ('lo que analizarás' if i < 3 else 'los grupos a encontrar'))
ax.scatter(X[:,0], X[:,1], s=15, c=y, alpha=.3)
ax.axis('equal')
plt.tight_layout()
En este apartado se pide probar el algoritmo k-means sobre los tres datasets presentados anteriormente ajustando con los parámetros adecuados y analizar sus resultados.
X, y = X_blobs, y_blobs
Para estimar el número de clusters a detectar por k-means. Una técnica para estimar $k$ es, como se explica en la teoría:
Los criterios anteriores (minimización de distancias intra grupo o maximización de distancias inter grupo) pueden usarse para establecer un valor adecuado para el parámetro k. Valores k para los que ya no se consiguen mejoras significativas en la homogeneidad interna de los segmentos o la heterogeneidad entre segmentos distintos, deberían descartarse.
Lo que popularmente se conocer como regla del codo.
Primero es necesario calcular la suma de los errores cuadráticos (SSE) que consiste en la suma de todos los errores (distancia de cada punto a su centroide asignado) al cuadrado.
$$SSE = \sum_{i=1}^{K} \sum_{x \in C_i} euclidean(x, c_i)^2$$Donde $K$ es el número de clusters a buscar por k-means, $x \in C_i$ son los puntos que pertenecen a i-ésimo cluster, $c_i$ es el centroide del cluster $C_i$ (al que pertenece el punto $x$), y $euclidean$ es la distancia euclídea.
Este procedimiento realizado para cada posible valor $k$, resulta en una función monótona decreciente, donde el eje $x$ representa los distintos valores de $k$, y el eje $y$ el $SSE$. Intuitivamente se podrá observar un significativo descenso del error, que indicará el valor idóneo de $k$.
Se pide realizar la representación gráfica de la regla del codo junto a su interpretación, utilizando la librería matplotlib y la implementación en scikit-learn de k-means.
sse = []
for k in range(1, 11):
kmeans = cluster.KMeans(n_clusters=k, random_state=42)
kmeans.fit(X)
sse.append(kmeans.inertia_)
# Representar gráficamente la regla del codo
plt.figure(figsize=(8, 6))
plt.plot(range(1, 11), sse, marker='o')
plt.title('Regla del Codo para k-means')
plt.xlabel('Número de Clusters (k)')
plt.ylabel('SSE (Suma de Errores Cuadráticos)')
plt.grid(True)
plt.show()
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10)
En la grafica se puede observar la representacion de la regla de codo para la eleccion de K cluster para este algoritmo. En el podemos sacar la conclusion de que el mejor numero de cluster es 4 (k=4) debidoa que en el se reduce considerablemente el error y, ademas, no se aprecia una reduccion del error importante al aumentar el numero de clusters.
Al realizar el algoritmo k-means, este se centra en minimizar la distancia intra-grupo utilizando la distancia euclidean. Por lo que, para mejor la eleccion de k se podria utilizar otra forma que incluyera la distancia inter-grupo.
# Configurar el gráfico
fig, ax = plt.subplots(1, 1, figsize=(5, 5))
# Configurar y ajustar el modelo K-means
kmeans = cluster.KMeans(n_clusters=4, algorithm='full')
labels = kmeans.fit_predict(X)
# Visualizar los puntos de datos y los centroides
scatter = ax.scatter(X[:, 0], X[:, 1], c=labels, cmap='jet')
centers = ax.scatter(
kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
s=300, c='k', marker='X', linewidths=2, edgecolors='w'
)
# Añadir leyenda
ax.legend([scatter, centers], ['Puntos de Datos', 'Centroides'])
# Configurar el aspecto del gráfico
ax.axis('equal')
plt.tight_layout()
# Mostrar el gráfico
plt.show()
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1416: FutureWarning: algorithm='full' is deprecated, it will be removed in 1.3. Using 'lloyd' instead. warnings.warn(
En el metodo estandar utilizado de k means se asume la distancia euclidea. Este metodo elige los centroides aleatoriamente y se van ajustando al que minimice el error. Como se toma de referencia el centroide de los cluster, la distancia euclidea asume que los cluster van a tener una forma "circular" por lo que en este caso ha separado bastante bien los datos.
Cabe destacar que en este caso ha sido correcto utilizar esta distancia, pero en la practica podemos encontrarnos con conjuntos de datos para clasifica donde no funcione correctamente o el resultado no sea eficiente de este algoritmo utilizado
X, y = X_moons, y_moons
sse = []
for k in range(1, 11):
kmeans = cluster.KMeans(n_clusters=k, random_state=42)
kmeans.fit(X)
sse.append(kmeans.inertia_)
# Representar gráficamente la regla del codo
plt.figure(figsize=(8, 6))
plt.plot(range(1, 11), sse, marker='o')
plt.title('Regla del Codo para k-means')
plt.xlabel('Número de Clusters (k)')
plt.ylabel('SSE (Suma de Errores Cuadráticos)')
plt.grid(True)
plt.show()
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10)
En el caso del dataset moon, podemos observar que no es facil la eleccion de K con el metodo del codo y no coincide con la observacion de los datos al principio del ejercicio donde se esperaban 2 cluster perfectamente diferenciados. Para poder mejorar la eleccion de K se podria utilizar otro metodo como el coeficiente de silueta o utilizar otros metodos diferentes a kmeans.
# Configurar el gráfico
fig, ax = plt.subplots(1, 1, figsize=(5, 5))
# Configurar y ajustar el modelo K-means
kmeans = cluster.KMeans(n_clusters=2, algorithm='full')
labels = kmeans.fit_predict(X)
# Visualizar los puntos de datos y los centroides
scatter = ax.scatter(X[:, 0], X[:, 1], c=labels, alpha=0.8, cmap='jet')
centers = ax.scatter(
kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
s=350, c='k', marker='X', linewidths=2, edgecolors='w'
)
# Añadir leyenda
ax.legend([scatter, centers], ['Puntos de Datos', 'Centroides'])
# Configurar el aspecto del gráfico
ax.axis('equal')
plt.tight_layout()
# Mostrar el gráfico
plt.show()
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1416: FutureWarning: algorithm='full' is deprecated, it will be removed in 1.3. Using 'lloyd' instead. warnings.warn(
En este caso, se observar que el algoritmo ha clasificado de forma erronea los datos en dos cluster. Esto se debe a la utilizacion de kmeans con la distancia euclidea ya que este clasifica los datos segun la distancia a los centroides haciendolos con forma circular. En este caso como no tienen forma circular los datos congregados se ha producido el error.
Bloque con sangría
Para poder encontrar los grupos de este conjunto de datos de la mejor forma se podrian utilizar metricas de distancia como manhattan o chebyshev y probarlos.
De la misma manera, existe otro algoritmo de agrupamiento llamado DBSCAN el cual se basa en la densidad de los datos.
X, y = X_circles, y_circles
sse = []
for k in range(1, 11):
kmeans = cluster.KMeans(n_clusters=k, random_state=42)
kmeans.fit(X)
sse.append(kmeans.inertia_)
# Representar gráficamente la regla del codo
plt.figure(figsize=(8, 6))
plt.plot(range(1, 11), sse, marker='o')
plt.title('Regla del Codo para k-means')
plt.xlabel('Número de Clusters (k)')
plt.ylabel('SSE (Suma de Errores Cuadráticos)')
plt.grid(True)
plt.show()
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10)
En este caso ocurre como con el dataset moon, deberia de encontrar que la mejor eleccion de K serian de 2 pero en este caso no se encuentra clara la eleccion de k.
# Configurar el gráfico
fig, ax = plt.subplots(1, 1, figsize=(5, 5))
# Configurar y ajustar el modelo K-means
kmeans = cluster.KMeans(n_clusters=2, algorithm='full')
labels = kmeans.fit_predict(X)
# Visualizar los puntos de datos y los centroides
scatter = ax.scatter(X[:, 0], X[:, 1], c=labels, alpha=0.8, cmap='jet')
centers = ax.scatter(
kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
s=350, c='k', marker='X', linewidths=2, edgecolors='w'
)
# Añadir leyenda
ax.legend([scatter, centers], ['Puntos de Datos', 'Centroides'])
# Configurar el aspecto del gráfico
ax.axis('equal')
plt.tight_layout()
# Mostrar el gráfico
plt.show()
/Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1412: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning super()._check_params_vs_input(X, default_n_init=10) /Users/urimossinger/anaconda3/lib/python3.11/site-packages/sklearn/cluster/_kmeans.py:1416: FutureWarning: algorithm='full' is deprecated, it will be removed in 1.3. Using 'lloyd' instead. warnings.warn(
Sucede igual que el anterior, la distancia que se utiliza no es la forma correcta para clasificar estos datos y se deberia de buscar una forma mas eficiente.
En este apartado se pide aplicar clustering por densidad como DBSCAN a los datasets anteriores para detectar los dos grupos subyacentes.
Ésta es una visualización intuitiva de su funcionamiento: https://www.youtube.com/watch?v=RDZUdRSDOok
X, y = X_blobs, y_blobs
mod_dbs = cluster.DBSCAN(eps=2.5, min_samples=140)
clusters = mod_dbs.fit(X)
fig, ax = plt.subplots(1, 1, figsize=(5, 5))
ax.scatter(X[:,0], X[:,1], c=clusters.labels_, alpha=.3, cmap='jet')
ax.axis('equal')
plt.tight_layout()
Al cambiar el algoritmo de clasificacion a DBSCAN encontramos unos parametros que podemos controlar para encontrar los cluster:
eps: este parametro es significa la disctancia maxima entre dos puntos para que sean considerados vecinos. Se utiliza para controlar la sensibilidad del algoritmo a la densidad.
min_samples: se utiliza para controlar el numero minimo de muestras en la vecindad para que se considere un punto central. Aumentar este parametro requiere que mas puntos esten cerca para formar un cluster.
En este caso se encuentran los 4 clusters utilizando eps aproximado de 2.5 y min samples aproximado de 140 con sus respectivos outliers. Esto se debe a que existe mucha densidad en sus centros (eps) y se encuentran bien separados (min_samples)
X, y = X_moons, y_moons
model = cluster.DBSCAN(eps=0.1, min_samples=13, n_jobs=-1)
clusters = model.fit(X)
fig, ax = plt.subplots(1, 1, figsize=(5, 5))
ax.scatter(X[:,0], X[:,1], c=clusters.labels_, alpha=.3, cmap='jet')
ax.axis('equal')
plt.tight_layout()
En este caso se encuentran los 2 clusters utilizando eps aproximado de 0.1 y min samples aproximado de 13 con sus respectivos outliers. Esto se debe a que existe una densidad constante a lo largo de la forma que construyen y la distancia entre los dos es suficientemente grande.
X, y = X_circles, y_circles
model = cluster.DBSCAN(eps=0.1, min_samples=5, n_jobs=-1)
clusters = model.fit(X)
fig, ax = plt.subplots(1, 1, figsize=(5, 5))
ax.scatter(X[:,0], X[:,1], c=clusters.labels_, alpha=.3, cmap='jet')
ax.axis('equal')
plt.tight_layout()
En este caso se encuentran los 2 clusters utilizando eps aproximado de 0.1 y min samples aproximado de 5 con sus respectivos outliers. Esto se debe a que existe una densidad constante a lo largo de la forma que construyen y la distancia entre los dos es suficientemente grande. Pasa exactamente igual que con moon.
En este apartado se pide visualizar mediante un dendrograma la construcción progresiva de los grupos mediante un algoritmo jerárquico aglomerativo (estrategia bottom-up). Con ello se pretende encontrar un método gráfico para entender el comportamiento del algoritmo y encontrar los clusters deseados en cada dataset.
X, y = X_blobs, y_blobs
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
from scipy.spatial.distance import pdist
# Suponiendo que ya tienes X_blobs definido
# Lista de métodos de enlace a probar
linkage_methods = ['single', 'complete', 'average', 'weighted', 'centroid', 'median', 'ward']
# Iterar sobre los métodos de enlace
for method in linkage_methods:
# Calcular la matriz de enlace usando el método actual
linkage_matrix = linkage(pdist(X), method=method)
# Visualizar el dendrograma
fig, ax = plt.subplots(1, 2, figsize=(11, 5))
dendrogram(linkage_matrix, ax=ax[0])
ax[0].set_title(f'Dataset Blobs con linkage "{method}"')
# Asignar clusters usando fcluster con el umbral de distancia especificado
clusters = fcluster(linkage_matrix, t=15, criterion='distance')
# Visualizar los puntos de datos coloreados por cluster
ax[1].scatter(X[:, 0], X[:, 1], c=clusters, cmap='jet')
ax[1].axis('equal')
ax[1].set_title('Clusters identificados')
plt.tight_layout()
# Mostrar el gráfico
plt.show()
En este casi el mejor enlace es el "complete" debido a que utiliza la maximizacion de las distancia entre los diferentes puntos del cluster, de esta manera consigue distinguir los 4 cluster desde sus centros.
X, y = X_moons, y_moons
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
from scipy.spatial.distance import pdist
# Suponiendo que ya tienes X_blobs definido
# Lista de métodos de enlace a probar
linkage_methods = ['single', 'complete', 'average', 'weighted', 'centroid', 'median', 'ward']
# Iterar sobre los métodos de enlace
for method in linkage_methods:
# Calcular la matriz de enlace usando el método actual
linkage_matrix = linkage(pdist(X), method=method)
# Visualizar el dendrograma
fig, ax = plt.subplots(1, 2, figsize=(11, 5))
dendrogram(linkage_matrix, ax=ax[0])
ax[0].set_title(f'Dataset Moon con linkage "{method}"')
# Asignar clusters usando fcluster con el umbral de distancia especificado
clusters = fcluster(linkage_matrix, t=.2, criterion='distance')
# Visualizar los puntos de datos coloreados por cluster
ax[1].scatter(X[:, 0], X[:, 1], c=clusters, cmap='jet')
ax[1].axis('equal')
ax[1].set_title('Clusters identificados')
plt.tight_layout()
# Mostrar el gráfico
plt.show()
En este caso el enlace simple es que mejor se adapta para poder realizar la division de los dataset. Esto sucede debido a que utilizada la distancia minima entre los puntos mas proximos y encontrar los cluster que presentan formas diferentes a esferas.
X, y = X_circles, y_circles
import matplotlib.pyplot as plt
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
from scipy.spatial.distance import pdist
# Suponiendo que ya tienes X_blobs definido
# Lista de métodos de enlace a probar
linkage_methods = ['single', 'complete', 'average', 'weighted', 'centroid', 'median', 'ward']
# Iterar sobre los métodos de enlace
for method in linkage_methods:
# Calcular la matriz de enlace usando el método actual
linkage_matrix = linkage(pdist(X), method=method)
# Visualizar el dendrograma
fig, ax = plt.subplots(1, 2, figsize=(11, 5))
dendrogram(linkage_matrix, ax=ax[0])
ax[0].set_title(f'Dataset Circles con linkage "{method}"')
# Asignar clusters usando fcluster con el umbral de distancia especificado
clusters = fcluster(linkage_matrix, t=0.2, criterion='distance')
# Visualizar los puntos de datos coloreados por cluster
ax[1].scatter(X[:, 0], X[:, 1], c=clusters, cmap='jet')
ax[1].axis('equal')
ax[1].set_title('Clusters identificados')
plt.tight_layout()
# Mostrar el gráfico
plt.show()
En este caso el enlace simple es que mejor se adapta para poder realizar la division de los dataset. Esto sucede debido a que utilizada la distancia minima entre los puntos mas proximos y encontrar los cluster que presentan formas diferentes a esferas.
En ajedrez existen multitud de aperturas y variantes. Te permiten planificar como posicionarás tus piezas, lo cual puede otorgar una gran ventaja durante el desarrollo de la partida. Hay tantas aperturas distintas (cada una de ellas con sus variantes) que puede ser dificil situarte, como puede apreciarse en el siguiente video del Maestro FIDE Luis Fernández.
Como muchas aperturas se parecen a otras, porque tienen planes similares, una buena forma para ubicarse es saber cuales se parecen entre sí. Y esa es la idea de este análisis.
Partiremos de un dataset de partidas de ajedrez en la plataforma lichess, que consta de los siguientes campos (se resaltan los útiles para el análisis):
Se carga el dataset en un dataframe de pandas:
df = pd.read_csv('games.csv')
print(df.head())
id rated created_at last_move_at turns victory_status winner \
0 TZJHLljE False 1.504210e+12 1.504210e+12 13 outoftime white
1 l1NXvwaE True 1.504130e+12 1.504130e+12 16 resign black
2 mIICvQHh True 1.504130e+12 1.504130e+12 61 mate white
3 kWKvrqYL True 1.504110e+12 1.504110e+12 61 mate white
4 9tXo1AUZ True 1.504030e+12 1.504030e+12 95 mate white
increment_code white_id white_rating black_id black_rating \
0 15+2 bourgris 1500 a-00 1191
1 5+10 a-00 1322 skinnerua 1261
2 5+10 ischia 1496 a-00 1500
3 20+0 daniamurashov 1439 adivanov2009 1454
4 30+3 nik221107 1523 adivanov2009 1469
moves opening_eco \
0 d4 d5 c4 c6 cxd5 e6 dxe6 fxe6 Nf3 Bb4+ Nc3 Ba5... D10
1 d4 Nc6 e4 e5 f4 f6 dxe5 fxe5 fxe5 Nxe5 Qd4 Nc6... B00
2 e4 e5 d3 d6 Be3 c6 Be2 b5 Nd2 a5 a4 c5 axb5 Nc... C20
3 d4 d5 Nf3 Bf5 Nc3 Nf6 Bf4 Ng4 e3 Nc6 Be2 Qd7 O... D02
4 e4 e5 Nf3 d6 d4 Nc6 d5 Nb4 a3 Na6 Nc3 Be7 b4 N... C41
opening_name opening_ply
0 Slav Defense: Exchange Variation 5
1 Nimzowitsch Defense: Kennedy Variation 4
2 King's Pawn Game: Leonardis Variation 3
3 Queen's Pawn Game: Zukertort Variation 3
4 Philidor Defense 5
El primer paso al tratar con dato real es analizar el dato para comprender el dominio, y aplicar determinados filtrados en base a la lógica de tu tarea.
df = df[df['opening_ply'] >= 4]
df = df[df['turns'] / 2 > df['opening_ply']]
df['opening_name'] = df['opening_name'].apply(lambda x: x.split('|')[0].strip())
df_new = df['opening_name'].value_counts() >= 40
df_top = set(df_new.index[df_new])
df = df[df['opening_name'].apply(lambda x: x in df_top)]
AVISO: ¡no es necesario saber interpretar correctamente la notación algebraica para el desarrollo de la práctica!
Los movimientos en ajedrez se pueden transcribir de distintas maneras, la más popular es la notación algebraica. En este caso, la notación algebraica empleada es la inglesa.
En concreto, el campo moves tiene los movimientos alternos tanto de las blancas como de las negras. Por tanto, la secuencia:
e4 c5 Nf3 Qa5...
Se interpretaría como:
Y así alternan movimientos hasta el final de la partida (victoria, tablas, abandono o quedarse sin tiempo).
Es importante tener en cuenta que el campo moves es de tipo string, por lo que será necesario dividirlo por el separador (espacio) para tener cada movimiento por separado. Útil para los siguientes pasos.
df['white_moves'] = df.apply(
lambda x: [m for i, m in enumerate(x.moves.split(' ')) if i % 2 == 0 and i < x.opening_ply * 2],
axis=1)
Para comparar las aperturas entre sí usaremos los movimientos empleados en ella sólo por parte del jugador blanco (sólo hay nombre de la apertura del jugador blanco). Por simplicidad, podemos ignorar su orden, por lo que podemos usar la estrategia de bag of words. Generando, a partir del campo white_moves un nuevo dataset con tantas dimensiones como posibles movimientos con un 1 si se ha realizado durante la apertura y un 0 si no se ha realizado.
Puedes crear nuevas columnas a partir de valores con el método get_dummies() de pandas.
Si el campo a partir del cual quieres generar las dimensiones es una lista de strings, aquí hay una pista.
data_bag = df['white_moves'].str.join('|').str.get_dummies()
En este punto tienes un dataset con tantas filas como partidas y tantas columnas como movimientos posibles efectuados en el conjunto de datos.
El problema es que ahora dispones de muchas dimensiones, por lo que para visualizar los datos y comprobar si hay algún tipo de estructura es necesario reducir su dimensionalidad. Obteniendo un embedding (representación compacta) de las aperturas.
La reducción de dimensionalidad puede llevarse a cabo con métodos como PCA. Pero este método tiene la limitación de que sólo realiza proyecciones lineales. Por lo que otros métodos como t-SNE o UMAP pueden ofrecer mejores resultados.
model = umap.UMAP()
data_map = model.fit_transform(data_bag)
import matplotlib.pyplot as plt
# Crear un scatter plot
plt.scatter(data_map[:, 0], data_map[:, 1], c='b', marker='o', s=10)
# Configurar etiquetas y título
plt.title('Visualización UMAP de los Datos en 2D')
plt.xlabel('Dimensión 1')
plt.ylabel('Dimensión 2')
# Mostrar el gráfico
plt.show()
import plotly.graph_objects as go
fig = go.Figure(
data=[go.Scatter(
x=data_map[:, 0],
y=data_map[:, 1],
mode='markers',
hovertext=df['opening_name'],
marker=dict(
color='LightSkyBlue',
size=4,
opacity=0.2,
)
)]
)
fig.update_layout(
paper_bgcolor='white',
plot_bgcolor='white',
)
fig.show()
Tras observar la estructura de las muestras en baja dimensionalidad.
Viendo las graficas de los datos puede funcionar el algoritmo HBSCAN, ya que se bansa en la densidad de los datos. Se puede observar que los grupos mantienen diferente tamaño por lo que son irregulares y este algoritmo podria funcionar
import hdbscan
model = hdbscan.HDBSCAN(min_cluster_size=60)
model.fit(data_map)
model.labels_.max()
model.condensed_tree_.plot()
<Axes: ylabel='$\\lambda$ value'>
fig = go.Figure(
data=[go.Scatter(x=data_map[model.labels_ >= 0,0],
y=data_map[model.labels_ >= 0,1],
mode='markers',
hovertext=df['opening_name'],
marker=dict(
color=model.labels_[model.labels_ >= 0],
size=5,
opacity=0.2,
colorscale=px.colors.qualitative.Light24
),
)],
layout=dict(title="Opening's embedding",
width=900,
height=800,
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
xaxis=dict(
linecolor='#cccccc',
linewidth=1,
zeroline=False,
visible=True
),
yaxis=dict(
linecolor='#cccccc',
linewidth=1,
zeroline=False,
visible=True
),
))
fig.show()
fig, axis = plt.subplots(model.labels_.max() + 1, 1, figsize=(10, 100))
for ax, i in zip(axis.squeeze(), range(model.labels_.max() + 1)):
df[model.labels_ == i]['opening_name'].value_counts().plot(kind='barh', ax=ax, title=f'Cluster {i}')
plt.tight_layout()
Una manera habitual de comparar variables (en este caso aperturas) es emplear una matriz de distancias. Donde podemos representar cuál es la distancia mínima, media o máxima entre dos aperturas.
Dado que cada apertura tiene distintas muestras (al menos 40). Si usamos el mínimo y tuviésemos 5 muestras de la apertura A y 3 de la B. La distancia entre las aperturas A y B sería la mínima de las 15 distancias posibles que hay entre las 5 muestras de A y las 3 de B.
import numpy as np
import pandas as pd
from tqdm import tqdm
import plotly.express as px
rnd = 0
# Asumiendo que df es tu DataFrame y que contiene una columna 'opening_name'
# y 'data_map' es un array o una lista de las coordenadas
op2idx = {op: i for i, op in enumerate(df['opening_name'].value_counts().index)}
dists = np.full((len(op2idx), len(op2idx)), np.inf) # Usamos np.inf como valor inicial para la distancia máxima
# Asumimos que 'data_map' es un arreglo NumPy con las coordenadas de las muestras
# data_map = ... (deberás definir esto según tus datos)
# Iteramos sobre todas las combinaciones posibles de aperturas para calcular la distancia euclidiana mínima
for i, (_, op_a) in tqdm(enumerate(df['opening_name'].items()), total=df.shape[0]):
for j, (_, op_b) in enumerate(df['opening_name'].items()):
idx_i = op2idx[op_a]
idx_j = op2idx[op_b]
if idx_i == idx_j:
dists[idx_i, idx_j] = 0.
elif rnd == 0 or dists[idx_i, idx_j] == np.inf:
d = np.linalg.norm(data_map[i] - data_map[j])
dists[idx_i, idx_j] = d
dists[idx_j, idx_i] = d # Aprovechamos la simetría de la matriz de distancias
rnd = (rnd + 1) % 4
# Convertimos la matriz de distancias a un DataFrame para facilitar la visualización
distance_df = pd.DataFrame(dists, index=sorted(op2idx, key=op2idx.get), columns=sorted(op2idx, key=op2idx.get))
# Visualizamos la matriz de distancias con Plotly
fig = px.imshow(distance_df, color_continuous_scale='RdBu', width=1900, height=1900)
fig.show()
100%|██████████████████████████████████████| 6975/6975 [00:39<00:00, 175.87it/s]
Después de llevar a cabo el análisis de clustering mediante HDBSCAN y la generación de una matriz de distancias entre clusters, podemos extraer las siguientes conclusiones:
Estructura en Baja Dimensionalidad: La representación visual a través de UMAP revela patrones en nuestros datos. La agrupación en diferentes conjuntos puede indicar diferencias en las características de las muestras, sugiriendo que la reducción de dimensionalidad ha capturado eficientemente la esencia de los datos.
Clustering con HDBSCAN: La implementación de HDBSCAN parece haber sido exitosa al lidiar con la complejidad estructural de nuestros datos, generando grupos bien definidos que destacan las similitudes entre las muestras.
Heatmap: El heatmap exhibe la variabilidad en la proximidad entre clusters. Las disparidades en la intensidad de los colores señalan que la proximidad entre clusters no es uniforme, indicando una estructura de datos rica y posiblemente compleja. Algunos clusters muestran una mayor afinidad entre sí, sugiriendo la existencia de aperturas o estilos de juego específicos.
Para comprender mejor lo analizado, puedes visualizar las aperturas de las muestras mediante la librería chess que facilita mucho la tarea.
Para ello selecciona una muestra (más fácil) o un tipo de apertura, y de la columna moves selecciona 2 opening_ply* movimientos de la apertura (el x2 es para coger los de las blancas y las negras). Una vez que ya los tengas puedes crear un tablero con la librería chess:
board = chess.Board()
Tras ello, simplemente itera por esos primeros y pásaselos al tablero mediante el método push_san con:
board.push_san('d4')
hasta agotar los movimientos SOLO de la apertura de blancas y negras en el mismo orden en el que ya aparecen en el campo moves.
!pip install chess
Requirement already satisfied: chess in /Users/urimossinger/anaconda3/lib/python3.11/site-packages (1.10.0)
import chess
def movimientos(df: pd.DataFrame, name: str) -> chess.Board():
aux = df[df['opening_name'] == name].sample()
board = chess.Board()
print(name)
for x in aux['moves'].values[0].split(' ')[:aux['opening_ply'].values[0] * 2]:
board.push_san(x)
return board
movimientos(df, "Queen's Pawn Game: Colle System")
Queen's Pawn Game: Colle System